package io.hellsinger.filesystem.checksum

import io.hellsinger.filesystem.path.Path
import io.hellsinger.filesystem.path.toFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.security.MessageDigest

object ChecksumGenerator {
    const val MD2 = "MD2"
    const val MD5 = "MD5"
    const val SHA_1 = "SHA-1"
    const val SHA_224 = "SHA-224"
    const val SHA_256 = "SHA-256"
    const val SHA_384 = "SHA-384"
    const val SHA_512 = "SHA-512"
    const val SHA_512_224 = "SHA-512/224"
    const val SHA_512_256 = "SHA-512/256"
    const val SHA3_224 = "SHA3-224"
    const val SHA3_256 = "SHA3-256"
    const val SHA3_384 = "SHA3-384"
    const val SHA3_512 = "SHA3-512"

    private val DIGITS_LOWER =
        charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')

    private val DIGITS_UPPER =
        charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')

    suspend fun generate(
        path: Path,
        algorithm: String,
        toLowerCase: Boolean = false,
    ) = generate(
        path,
        MessageDigest.getInstance(algorithm),
        toLowerCase,
    )

    suspend fun generate(
        path: Path,
        digest: MessageDigest,
        toLowerCase: Boolean = false,
    ) = withContext(Dispatchers.IO) {
        val input = FileInputStream(path.toFile())

        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
        var read = input.read(buffer, 0, DEFAULT_BUFFER_SIZE)

        while (read > -1) {
            digest.update(buffer, 0, read)
            read = input.read(buffer, 0, DEFAULT_BUFFER_SIZE)
        }

        val bytes = digest.digest()
        input.close()
        String(encodeHex(bytes, toLowerCase))
    }

    private fun encodeHex(
        data: ByteArray,
        toLowerCase: Boolean,
    ): CharArray = encodeHex(data, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER)

    private fun encodeHex(
        data: ByteArray,
        toDigits: CharArray,
    ): CharArray {
        val l = data.size
        val out = CharArray(l shl 1)
        var i = 0
        var j = 0
        while (i < l) {
            out[j++] = toDigits[0xF0 and data[i].toInt() ushr 4]
            out[j++] = toDigits[0x0F and data[i].toInt()]
            i++
        }
        return out
    }
}
